
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;

/**
 * 服务器端代码
 */
public class CloudService implements Runnable {
    private static final String HEAD_STRING = "Santoker Share v0.1";
    private static final byte[] HEAD_BYTES = "Santoker Share v0.1\n".getBytes();
    private static final byte[] REQUEST_END_BYTES = "Request End\n".getBytes();
    private static final byte[] RESPONSE_END_BYTES = "Response End\n".getBytes();
    private static final byte[] RESPONSE_HEAD_OK_BYTES = "Santoker Share v0.1\nOK\n".getBytes();

    private static final int MAX_INPUT_SIZE = 30000;
    private static final int MAX_RESPONSE_KEYS = 200;
    private static final int WAIT_RESPONSE_TIMEOUT = 5000;
    private static final String METHOD_GET = "Get";
    private static final String METHOD_SAVE = "Save";
    private static final String METHOD_KEYS = "Keys";
    private static final String METHOD_SHARE = "Share";
    private static final String METHOD_STATISTIC = "Statistic";

    private static int size, count, dead;
    private static final Random random = new Random();
    private static final HashMap<Integer, Data> map = new HashMap<>();
    private static final DataCleaner dataCleaner = new DataCleaner(map);

    private final Socket socket;

    private CloudService(Socket socket) {
        this.socket = socket;
    }

    public static void main(String[] args) throws Exception {

        // load saved data
        if (args.length > 0) {
            for (String file : args) {
                System.out.println("Loading " + file);

                FileInputStream is = new FileInputStream(file);
                ObjectInputStream ois = new ObjectInputStream(is);
                Map<? extends Integer, ? extends Data> saved =
                        (Map<? extends Integer, ? extends Data>) ois.readObject();

                map.putAll(saved);
                ois.close();
                is.close();
            }
            count = map.size();
            Set<Map.Entry<Integer, Data>> values = map.entrySet();
            for (Map.Entry<Integer, Data> v : values) {
                byte[] data = v.getValue().getData();

                if (data != null) size += data.length;
                else ++dead;
            }
            System.out.println("Saved data loaded.");
        }

        // run data-clean thread
        (new Thread(dataCleaner)).start();

        // start service
        ServerSocket server = new ServerSocket(49936);
        System.out.println("Server Started");
        while (true) {
            Socket connection = server.accept();
            (new Thread(new CloudService(connection))).start();
        }
    }

    private static void sleepUntil(long millis) {
        long startMillis, currentMillis;
        startMillis = currentMillis = System.currentTimeMillis();
        do {
            try {
                Thread.sleep(startMillis + millis - currentMillis);
            } catch (InterruptedException ignored) {
            }
            currentMillis = System.currentTimeMillis();
        } while (currentMillis - startMillis < millis);
    }

    private static boolean isEnd(byte[] data, int length) {
        int base = length - REQUEST_END_BYTES.length;
        if (base < 0) return false;

        for (int i = 0; i < REQUEST_END_BYTES.length; ++i)
            if (REQUEST_END_BYTES[i] != data[base + i])
                return false;

        return true;
    }

    private boolean doRequest(byte[] data, int length) throws Exception {
        int flag = 0, begin = 0, end = 0;
        for (int i = 0; i < length && flag < 2; ++i) {
            if (data[i] == '\n') {
                ++flag;

                if (flag == 1)
                    begin = i + 1;
                else if (flag == 2)
                    end = i;
            }
        }

        if (flag == 2) {
            String method = new String(data, begin, end - begin);
            if (METHOD_GET.equals(method))
                return doGetRequest(data, length);
            else if (METHOD_SHARE.equals(method))
                return doShareRequest(data, length, end + 1);
            else if (METHOD_STATISTIC.equals(method))
                return doStatisticRequest(data, length);
            else if (METHOD_SAVE.equals(method))
                return doSaveRequest(data, length);
            else if (METHOD_KEYS.equals(method))
                return doKeysRequest(data, length);
            else
                throw new Exception("Request method error");
        }
        return false;
    }

    private boolean doGetRequest(byte[] data, int length) throws IOException {
        if (!isEnd(data, length))
            return false;

        OutputStream out = socket.getOutputStream();
        String[] request = (new String(data, 0, length)).split("(?<!\\\\)\\n");
        if (request.length != 4 || !HEAD_STRING.equals(request[0])) {
            out.write(HEAD_BYTES);
            out.write("Request data error\n".getBytes());
            out.write(RESPONSE_END_BYTES);
        } else {

            // response
            int id = Integer.valueOf(request[2], 36);
            Data d = map.get(id);
            if (d == null) {
                out.write(HEAD_BYTES);
                out.write("连接不存在\n".getBytes());
                out.write(RESPONSE_END_BYTES);
            } else if (d.getData() == null) {
                out.write(HEAD_BYTES);
                out.write("连接已过期\n".getBytes());
                out.write(RESPONSE_END_BYTES);
            } else {
                out.write(RESPONSE_HEAD_OK_BYTES);
                out.write((d.getData().length + "\n").getBytes());
                out.write(d.getData());
            }

            // response end

        }

        return true;
    }

    private boolean doSaveRequest(byte[] data, int length) throws IOException {
        if (!isEnd(data, length))
            return false;

        OutputStream out = socket.getOutputStream();
        String[] request = (new String(data, 0, length)).split("(?<!\\\\)\\n");
        if (request.length != 3 || !HEAD_STRING.equals(request[0])) {
            out.write(HEAD_BYTES);
            out.write("Request data error\n".getBytes());
            out.write(RESPONSE_END_BYTES);
        } else {

            // response
            String str;
            synchronized (map) {
                try {
                    int day = (int) (System.currentTimeMillis() / (1000 * 60 * 60 * 24));
                    FileOutputStream outputStream = new FileOutputStream("./saved" + day);
                    ObjectOutputStream oos = new ObjectOutputStream(outputStream);
                    oos.writeObject(map);

                    oos.close();
                    outputStream.close();
                    str = "Saved\n";
                } catch (IOException e) {
                    str = e.getMessage() + '\n';
                }
            }

            out.write(RESPONSE_HEAD_OK_BYTES);
            out.write(str.getBytes());
            out.write(RESPONSE_END_BYTES);
            // response end

        }

        return true;
    }

    private boolean doKeysRequest(byte[] data, int length) throws IOException {
        if (!isEnd(data, length))
            return false;

        OutputStream out = socket.getOutputStream();
        String[] request = (new String(data, 0, length)).split("(?<!\\\\)\\n");
        if (request.length != 3 || !HEAD_STRING.equals(request[0])) {
            out.write(HEAD_BYTES);
            out.write("Request data error\n".getBytes());
            out.write(RESPONSE_END_BYTES);
        } else {

            // response
            int count = 0;
            StringBuilder builder = new StringBuilder(map.size() > MAX_RESPONSE_KEYS
                    ? MAX_RESPONSE_KEYS * 7 + 11 : map.size() * 7);
            synchronized (map) {
                Set<Map.Entry<Integer, Data>> values = map.entrySet();
                for (Map.Entry<Integer, Data> v : values) {
                    if (count >= MAX_RESPONSE_KEYS) {
                        builder.append("...more...\n");
                        break;
                    }
                    if (v.getValue().getData() != null) {
                        builder.append(Integer.toString(v.getKey(), 36));
                        builder.append('\n');
                        ++count;
                    }
                }
            }

            out.write(RESPONSE_HEAD_OK_BYTES);
            out.write(builder.toString().getBytes());
            out.write(RESPONSE_END_BYTES);
            // response end

        }

        return true;
    }

    private boolean doShareRequest(byte[] data, int length, int begin) throws IOException {
        for (int i = begin; i < length; ++i) {
            if (data[i] == '\n') {
                try {
                    int dataLength = Integer.valueOf(new String(data, begin, i - begin));
                    if (length - i == dataLength + 1) {

                        // response
                        byte[] d = new byte[dataLength];
                        int id = random.nextInt() & 0x7FFFFFFF;
                        System.arraycopy(data, i + 1, d, 0, dataLength);
                        synchronized (map) {
                            ++count;
                            size += dataLength;
                            map.put(id, new Data(d));
                        }

                        OutputStream out = socket.getOutputStream();
                        out.write(RESPONSE_HEAD_OK_BYTES);
                        out.write((Integer.toString(id, 36) + "\n").getBytes());
                        out.write(RESPONSE_END_BYTES);
                        // response end

                        return true;
                    }
                } catch (NumberFormatException ignored) {
                }
            }
        }

        return false;
    }

    private boolean doStatisticRequest(byte[] data, int length) throws IOException {
        if (!isEnd(data, length))
            return false;

        OutputStream out = socket.getOutputStream();
        String[] request = (new String(data, 0, length)).split("(?<!\\\\)\\n");
        if (request.length != 3 || !HEAD_STRING.equals(request[0])) {
            out.write(HEAD_BYTES);
            out.write("Request data error\n".getBytes());
            out.write(RESPONSE_END_BYTES);
        } else {

            // response
            String response = "items : " + count + ", dead : " + dead + ", used in bytes : " + size + "\n";
            out.write(RESPONSE_HEAD_OK_BYTES);
            out.write(response.getBytes());
            out.write(RESPONSE_END_BYTES);
            // response end

        }

        return true;
    }

    @Override
    public void run() {
        try {
            // get request
            // wait up to {@link CloudService#WAIT_RESPONSE_TIMEOUT} secs
            int index = 0;
            long millis = System.currentTimeMillis();
            byte[] buffer = new byte[MAX_INPUT_SIZE];
            InputStream in = socket.getInputStream();
            while (!socket.isInputShutdown() || in.available() > 0) {
                if (in.available() > 0) {
                    index += in.read(buffer, index, MAX_INPUT_SIZE - index);

                    if (doRequest(buffer, index))
                        break;
                    if (index >= MAX_INPUT_SIZE)
                        throw new Exception("Data is too big");
                }

                if (System.currentTimeMillis() - millis >= WAIT_RESPONSE_TIMEOUT)
                    throw new Exception("Timeout");
                sleepUntil(333);
            }
        } catch (Exception e) {
            System.out.println(new Date() + " : " + e.getLocalizedMessage());
        } finally {
            try {
                if (socket != null) socket.close();
            } catch (Exception ignored) {
            }
        }
    }

    static class Data implements Serializable {
        private byte[] data;
        private final long date;

        private Data(byte[] data, long date) {
            this.data = data;
            this.date = date;
        }

        Data(byte[] data) {
            this.data = data;
            this.date = System.currentTimeMillis();
        }

        Data invalidate() {
            Data another = new Data(data, date);
            this.data = null;
            return another;
        }

        byte[] getData() {
            return data;
        }

        long getDate() {
            return date;
        }
    }

    static class DataCleaner implements Runnable {
        private static final long INTERVAL = 2 * 24 * 60 * 60 * 1000;
        private static final long ALIVE_TIME = 180L * (24 * 60 * 60 * 1000);

        private int index;
        private final HashMap<Integer, Data> map;

        DataCleaner(HashMap<Integer, Data> map) {
            this.map = map;
        }

        private void clean() {
            long current = System.currentTimeMillis();
            HashMap<Integer, Data> toDisk = new HashMap<>();

            // iterate and clean
            synchronized (map) {
                Set<Map.Entry<Integer, Data>> values = map.entrySet();
                for (Map.Entry<Integer, Data> v : values) {
                    Data data = v.getValue();
                    if (data.getData() != null && current - data.getDate() > ALIVE_TIME) {
                        ++dead;
                        size -= data.getData().length;

                        toDisk.put(v.getKey(), data.invalidate());
                    }
                }
            }

            // save deleted data to disk
            if (toDisk.size() > 0) {
                File directory = new File("./saved");
                boolean exists = directory.exists() || directory.mkdirs();

                if (exists) {
                    File file = new File(directory, String.valueOf(++index));
                    while (file.exists()) {
                        file = new File(directory, String.valueOf(++index));
                    }

                    try {
                        if (file.createNewFile()) {
                            FileOutputStream outputStream = new FileOutputStream(file);
                            ObjectOutputStream oos = new ObjectOutputStream(outputStream);
                            oos.writeObject(toDisk);

                            oos.close();
                            outputStream.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("cannot create directory \"./saved\"");
                }
            }
        }

        @Override
        public void run() {
            while (true) {
                clean();
                System.gc();
                sleepUntil(INTERVAL);
            }
        }
    }
}
